﻿namespace TfsReportRenderer.CommandLine
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;

    using ClosedXML.Excel;

    using TfsReportRenderer.ImageAdder;

    /// <summary>
    /// The EXCEL formatter for persisting and formatting the test result data into an XSLT file.
    /// </summary>
    public class ExcelFormatter
    {
        /// <summary>
        /// The column definitions for the excel sheet.
        /// </summary>
        private readonly ColumnDefinition[] columns = 
                {
                    ColumnDefinition.Create("Test Suite", x => x.TestSuiteName),
                    ColumnDefinition.Create("Test Run Date", x => x.TestIterationDate),
                    ColumnDefinition.Create("Test Case Name", x => x.TestCaseName),
                    ColumnDefinition.Create("Outcome", x => x.TestCaseOutcome),
                    ColumnDefinition.Create("Test Iteration Id", x => x.TestIterationId),
                    ColumnDefinition.Create("Test Step", x => x.TestCaseStepName),
                    ColumnDefinition.Create("Step Outcome", x => x.TestCaseStepOutcome),
                    ColumnDefinition.Create("Comment", x => x.TestCaseStepOutcomeComment),
                    ColumnDefinition.Create("Tested by", x => x.TestUser),
                    ColumnDefinition.Create("Screenshot", x => x.Image),
                };

        /// <summary>
        /// Writes the data into a file.
        /// </summary>
        /// <param name="data"> The data. </param>
        /// <param name="targetFile"> The target file path and name. </param>
        public void Write(IEnumerable<TestResultData> data, string targetFile)
        {
            var postSteps = new List<PostGenerationSteps>();
            using (var documentStream = new MemoryStream())
            {
                var plans = data.GroupBy(x => x.TestPlan);
                using (var book = new ClosedXML.Excel.XLWorkbook())
                {
                    foreach (var plan in plans)
                    {
                        var sheet = book.AddWorksheet(plan.Key);

                        WriteColumnTitles(this.columns, sheet, 5);
                        postSteps.AddRange(WriteData(this.columns, plan, sheet, 6));
                        ExpandColumns(this.columns, sheet);

                        ColorCells(
                            sheet,
                            x => x.Address.RowNumber == 5 ? XLColor.Gray :
                                (x.Address.ColumnNumber == 7 || x.Address.ColumnNumber == 4) 
                                    ?    x.GetValue<string>() == "Failed" ? XLColor.Red :
                                         x.GetValue<string>() == "Passed" ? XLColor.Green :
                                                                            XLColor.Yellow :
                                         x.Address.RowNumber % 2 == 1 ? XLColor.LightGray 
                                    : null);

                        MergeCells(5, sheet);
                    }

                    book.SaveAs(documentStream);
                }

                if (DeleteExistingFile(targetFile))
                {
                    return;
                }

                using (var file = File.OpenWrite(targetFile))
                {
                    documentStream.Seek(0, SeekOrigin.Begin);
                    documentStream.CopyTo(file);
                    documentStream.Flush();
                    file.Flush(true);
                }

                // TODO: need to fix excel OpenXML processing - cannot be interpreted correctly by Excel :-(
                ////foreach (var step in postSteps)
                ////{
                ////    step.Apply(targetFile);
                ////}
            }
        }

        /// <summary>
        /// Deletes an existing file if it does exist.
        /// </summary>
        /// <param name="targetFile"> The target file. </param>
        /// <returns> A value indicating whether an exception has been thrown. </returns>
        private static bool DeleteExistingFile(string targetFile)
        {
            if (!File.Exists(targetFile))
            {
                return false;
            }
            
            try
            {
                File.Delete(targetFile);
                return false;
            }
            catch (IOException ex)
            {
                Console.WriteLine("Cannot delete existing file [{0}] - message: [{1}]", targetFile, ex.Message);
                return true;
            }
        }

        /// <summary>
        /// Sets the background color of the cells of a sheet by according to the output of <paramref name="func"/>.
        /// </summary>
        /// <param name="sheet"> The sheet containing the cells. </param>
        /// <param name="func"> The function returning the desired color. </param>
        private static void ColorCells(IXLWorksheet sheet, Func<IXLCell, XLColor> func)
        {
            var rows = sheet.RowsUsed();
            var columns = sheet.ColumnsUsed();
            for (var row = rows.First().RowNumber(); row <= rows.Last().RowNumber(); row++)
            {
                for (var column = columns.First().ColumnNumber(); column <= columns.Last().ColumnNumber(); column++)
                {
                    var cell = sheet.Cell(row, column);
                    var color = func(cell);
                    if (color != null)
                    {
                        cell.Style.Fill.BackgroundColor = color;
                    }
                }
            }
        }

        /// <summary>
        /// Merges cells of a column that do contain identical values.
        /// </summary>
        /// <param name="maxColumn">Right-most index of the columns to merge.</param>
        /// <param name="sheet"> The sheet. </param>
        private static void MergeCells(int maxColumn, IXLWorksheet sheet)
        {
            var rows = sheet.RowsUsed();

            for (var columnIndex = 1; columnIndex <= maxColumn; columnIndex++)
            {
                for (var rowStart = rows.First().RowNumber(); rowStart < rows.Last().RowNumber(); rowStart++)
                {
                    var rowEnd = rowStart + 1;
                    while (Enumerable.Range(1, columnIndex).All(x => sheet.Cell(rowEnd, x).Value.ToString() == sheet.Cell(rowStart, x).Value.ToString()))
                    {
                        rowEnd++;
                    }

                    if (--rowEnd > rowStart)
                    {
                        var range = sheet.Range(rowStart, columnIndex, rowEnd, columnIndex);
                        range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Top;
                        range.Merge();
                    }

                    rowStart = rowEnd;
                }
            }
        }

        /// <summary>
        /// Expands the column widths to match the content.
        /// </summary>
        /// <param name="columns"> The columns to expand. </param>
        /// <param name="sheet"> The sheet. </param>
        private static void ExpandColumns(ICollection<ColumnDefinition> columns, IXLWorksheet sheet)
        {
            var col = 1;
            for (int index = 0; index < columns.Count; index++)
            {
                sheet.Column(col++).AdjustToContents();
            }
        }

        /// <summary>
        /// The write data to the excel sheet.
        /// </summary>
        /// <param name="columns"> The columns. </param>
        /// <param name="plan"> The test plan. </param>
        /// <param name="sheet"> The sheet to write to. </param>
        /// <param name="startRow"> The start row for the first line of data. </param>
        /// <returns> The post generation steps for tasks that need to be done by another library. </returns>
        private static IEnumerable<PostGenerationSteps> WriteData(ColumnDefinition[] columns, IEnumerable<TestResultData> plan, IXLWorksheet sheet, int startRow)
        {
            var row = startRow;

            var postGenSteps = new List<PostGenerationSteps>();

            foreach (var testResultData in plan)
            {
                var col = 1;
                foreach (var column in columns)
                {
                    var value = column.GetValue(testResultData);
                    if (value != null && value.GetType() == typeof(MemoryStream))
                    {
                        var stream = (MemoryStream)value;
                        var memoryStream = new MemoryStream();
                        stream.CopyTo(memoryStream);
                        postGenSteps.Add(new AddImage(row, col, sheet.Name, memoryStream));
                    }

                    sheet.Cell(row, col++).Value = value;
                }

                row++;
            }

            return postGenSteps;
        }

        /// <summary>
        /// Writes the column titles.
        /// </summary>
        /// <param name="columns"> The columns to write the titles for. </param>
        /// <param name="sheet"> The sheet. </param>
        /// <param name="row"> The row to write the titles. </param>
        private static void WriteColumnTitles(IEnumerable<ColumnDefinition> columns, IXLWorksheet sheet, int row)
        {
            var col = 1;
            foreach (var column in columns)
            {
                sheet.Cell(row, col++).Value = column.Title;
            }
        }
    }
}
